Remix Auth
Simple Authentication for Remix
Features
- Full Server-Side Authentication
- Complete TypeScript Support
- Strategy-based Authentication
- Easily handle success and failure
- Implement custom strategies
- Supports persistent sessions
Overview
Remix Auth is a complete open-source authentication solution for Remix.run applications.
Heavily inspired by Passport.js, but completely rewrote it from scratch to work on top of the Web Fetch API. Remix Auth can be dropped in to any Remix-based application with minimal setup.
As with Passport.js, it uses the strategy pattern to support the different authentication flows. Each strategy is published individually as a separate npm package.
Installation
To use it, install it from npm (or yarn):
npm install remix-auth
Also, install one of the strategies. A list of strategies is available in the Community Strategies discussion.
Usage
Remix Auth needs a session storage object to store the user session. It can be any object that implements the SessionStorage interface from Remix.
In this example I'm using the createCookieSessionStorage function.
import { createCookieSessionStorage } from "@remix-run/node";
export let sessionStorage = createCookieSessionStorage({
cookie: {
name: "_session",
sameSite: "lax",
path: "/",
httpOnly: true,
secrets: ["s3cr3t"],
secure: process.env.NODE_ENV === "production",
},
});
export let { getSession, commitSession, destroySession } = sessionStorage;
Now, create a file for the Remix Auth configuration. Here import the Authenticator
class and your sessionStorage
object.
import { Authenticator } from "remix-auth";
import { sessionStorage } from "~/services/session.server";
export let authenticator = new Authenticator<User>(sessionStorage);
The User
type is whatever you will store in the session storage to identify the authenticated user. It can be the complete user data or a string with a token. It is completely configurable.
After that, register the strategies. In this example, we will use the FormStrategy to check the documentation of the strategy you want to use to see any configuration you may need.
import { FormStrategy } from "remix-auth-form";
authenticator.use(
new FormStrategy(async ({ form }) => {
let email = form.get("email");
let password = form.get("password");
let user = await login(email, password);
return user;
}),
"user-pass"
);
Now that at least one strategy is registered, it is time to set up the routes.
First, create a /login
page. Here we will render a form to get the email and password of the user and use Remix Auth to authenticate the user.
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { authenticator } from "~/services/auth.server";
export default function Screen() {
return (
<Form method="post">
<input type="email" name="email" required />
<input
type="password"
name="password"
autoComplete="current-password"
required
/>
<button>Sign In</button>
</Form>
);
}
export async function action({ request }: ActionFunctionArgs) {
return await authenticator.authenticate("user-pass", request, {
successRedirect: "/dashboard",
failureRedirect: "/login",
});
};
export async function loader({ request }: LoaderFunctionArgs) {
return await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
};
With this, we have our login page. If we need to get the user data in another route of the application, we can use the authenticator.isAuthenticated
method passing the request this way:
let user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
let user = await authenticator.isAuthenticated(request);
if (user) {
} else {
}
Once the user is ready to leave the application, we can call the logout
method inside an action.
export async function action({ request }: ActionFunctionArgs) {
await authenticator.logout(request, { redirectTo: "/login" });
};
Advanced Usage
Custom redirect URL based on the user
Say we have /dashboard
and /onboarding
routes, and after the user authenticates, you need to check some value in their data to know if they are onboarded or not.
If we do not pass the successRedirect
option to the authenticator.authenticate
method, it will return the user data.
Note that we will need to store the user data in the session this way. To ensure we use the correct session key, the authenticator has a sessionKey
property.
export async function action({ request }: ActionFunctionArgs) {
let user = await authenticator.authenticate("user-pass", request, {
failureRedirect: "/login",
});
let session = await getSession(request.headers.get("cookie"));
session.set(authenticator.sessionKey, user);
let headers = new Headers({ "Set-Cookie": await commitSession(session) });
if (isOnboarded(user)) return redirect("/dashboard", { headers });
return redirect("/onboarding", { headers });
};
Changing the session key
If we want to change the session key used by Remix Auth to store the user data, we can customize it when creating the Authenticator
instance.
export let authenticator = new Authenticator<AccessToken>(sessionStorage, {
sessionKey: "accessToken",
});
With this, both authenticate
and isAuthenticated
will use that key to read or write the user data (in this case, the access token).
If we need to read or write from the session manually, remember always to use the authenticator.sessionKey
property. If we change the key in the Authenticator
instance, we will not need to change it in the code.
Reading authentication errors
When the user cannot authenticate, the error will be set in the session using the authenticator.sessionErrorKey
property.
We can customize the name of the key when creating the Authenticator
instance.
export let authenticator = new Authenticator<User>(sessionStorage, {
sessionErrorKey: "my-error-key",
});
Furthermore, we can read the error using that key after a failed authentication.
export async function loader({ request }: LoaderFunctionArgs) {
await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
let session = await getSession(request.headers.get("cookie"));
let error = session.get(authenticator.sessionErrorKey);
return json({ error }, {
headers:{
'Set-Cookie': await commitSession(session)
}
});
};
Remember always to use the authenticator.sessionErrorKey
property. If we change the key in the Authenticator
instance, we will not need to change it in the code.
Errors Handling
By default, any error in the authentication process will throw a Response object. If failureRedirect
is specified, this will always be a redirect response with the error message on the sessionErrorKey
.
If a failureRedirect
is not defined, Remix Auth will throw a 401 Unauthorized response with a JSON body containing the error message. This way, we can use the CatchBoundary component of the route to render any error message.
If we want to get an error object inside the action instead of throwing a Response, we can configure the throwOnError
option to true
. We can do this when instantiating the Authenticator
or calling authenticate
.
If we do it in the Authenticator,
it will be the default behavior for all the authenticate
calls.
export let authenticator = new Authenticator<User>(sessionStorage, {
throwOnError: true,
});
Alternatively, we can do it on the action itself.
import { AuthorizationError } from "remix-auth";
export async function action({ request }: ActionFunctionArgs) {
try {
return await authenticator.authenticate("user-pass", request, {
successRedirect: "/dashboard",
throwOnError: true,
});
} catch (error) {
if (error instanceof Response) return error;
if (error instanceof AuthorizationError) {
}
}
};
If we define both failureRedirect
and throwOnError
, the redirect will happen instead of throwing an error.